<?php namespace sacvoyage;

require_once("util.php");
require_once("Object.php");

use \ArrayAccess, \Iterator, \Exception;



class Pathfinder implements ArrayAccess, Iterator {

	public function __construct(&$element) {
		$this->_element=&$element;
	}


	public function set($paths) {
		foreach($paths as $path=>$value) {
			$pointer = &$this->_find($path, true);
			$pointer = $value;
		}
	}

	public function get($paths=null) {
		if($paths!==null) {
			$result=array();
			foreach($this->_walk($this->_element) as $path=>$value) {
				if(in_array($path, $paths)) {
					$result[$path]=$value;
				}
			}
			return $result;
		} else {
			return $this->_walk($this->_element);
		}
	}

	public function exists($path) {
		try {
			$this->_find($path, false);
			return true;
		} catch (Exception $e) {
			return false;
		}
	}

	public function offsetSet($path, $value) {
		$reference=&$this->_find($path, true);
		$reference=$value;
	}

	public function offsetGet($path) {
		try {
			return $this->_find($path, false);
		} catch(\Exception $e) {
			throw new \Exception("$path: not such path");
		}
	}

	public function offsetUnset($path) {
		throw new Exception('not implemented yet');
	}

	public function offsetExists($path) {
	  try {
		$this->_find($path, false, false);
		return true;
	  } catch (Exception $e) {
		  return false;
	  }
	}

	public function current() {
		return current($this->_paths);
	}

	public function next() {	
		return next($this->_paths);
	}

	public function rewind() {
		$this->_paths=$this->get();
		reset($this->_paths);
	}

	public function key() {
		return key($this->_paths);
	}

	public function valid() {
		return $this->key()!==null; // key returns null if not valid
	}
	
	public function equals($other, $paths=null) {
		$paths=$this->get($paths);
		$otherPaths=$other->get($paths);
		if(count($paths)!=count($otherPaths)) {
			return false;
		}
		foreach($paths as $path=>$value) {
			if($paths[$path]!=$otherPath[$path]) {
				return false;
			}
		}
		return true;
	}


   /* get pointer to $this->_element element matching JSON-path $path
	 *
	 * @param path path according to a subset of JSON-path-specification
	 * @param overwrite if true, all ambigous data will be overwritten
	 */
	private function &_find($path, $overwrite) {
		if (substr($path, 0, 1) == '$') {
			$path = substr($path, 1);
		}
		if ($path == '') {
			return $this->_element;
		}

		$branch = &$this->_element;
		$groups = array();
		// row regex: ^(?:\.(?P<_attr>[_A-Za-z][_A-Za-z0-9]*)|(?:\[(?P<_enum>[0-9]+)\])|(?:\['(?P<_assoc>(?:[^']|(?<=\\)')+)'\]))(?P<_rest>.*)$
		// matches: ".name", "[123]", "['2#^& es 2ls\'as df']"
		// but not: ".first name", "[]", "[asdf]", "['']"
		$regex = '/^(?:\.(?P<_attr>[_A-Za-z][_A-Za-z0-9]*)|(?:\[(?P<_index>[0-9]+)\])|(?:\[\'(?P<_assoc>(?:[^\']|(?<=\\\\)\')+)\'\]))(?P<_rest>.*)$/';
		while (preg_match($regex, $path, $groups)) {
			
			if (strlen($groups['_attr']) || strlen($groups['_assoc'])) {
				$attr=strlen($groups['_attr'])  ? $groups['_attr'] 
				      : str_replace("\\'", "'", $groups['_assoc']);

				if (!is_object($branch)){
					if($overwrite) {
						$branch=new Object();
					} else {
						throw new Exception('got '.gettype($branch).' while object expected');
					}
				}

				if(!isset($branch->{$attr})) {
					if($overwrite) {
						$branch->{$attr}=null;
					} else {
						throw new Exception('got '.gettype($branch).' while object expected');
					}
				}

				$branch = &$branch->{$attr};

			} elseif (strlen($groups['_index'])) {
				$index = (int) $groups['_index'];

				if (!is_array($branch)){
					if($overwrite) {
						$branch=array();
					} else {
						throw new Exception('got '.gettype($branch).' while array expected');
					}
				}

				if(!isset($branch[$index])) {
					if($overwrite) {
						$branch[$index]=null;
					} else {
						throw new Exception('got '.gettype($branch).' while array expected');
					}
				}
				ksort($branch);
				$branch = &$branch[$index];
				

			}
			
			$path = $groups['_rest'];
		}
		
		if(strlen($path)) { 
			throw new InputException("not a well-formed path"); 
		} else { // all $path is completely parsed
			return $branch;
		}

	}

    /*
	 * if $path is specified those value is returned or exception thrown
	 * if $path is not specified assoc array of type array($path=>$value, ...) will be returned
	 */
	private function _walk($element, $prefix="") {
		if (is_numeric($element) 
		|| is_string($element)
		|| is_null($element) 
		|| is_bool($element)) {
			return array($prefix => $element);
		} elseif (is_object($element)) {
			$paths = array();
			foreach ($element as $key => $value) {
				if (preg_match('/^[_A-Za-z][_A-Za-z0-9]*$/', $key)) {
					$paths = array_merge($paths, $this->_walk($value, $prefix . ".$key"));
				} else {
					$escapedKey = str_replace("'", "\\'", $key);
					$paths = array_merge($paths, $this->_walk($value, $prefix . "['$escapedKey']"));
				}
			}
			return $paths;
		} elseif (is_enumerative_array($element)) {
			$paths = array();
			foreach ($element as $index => $value) {
				$paths = array_merge($paths, $this->_walk($value, $prefix . "[$index]"));
			}
			return $paths;
		} elseif (is_associative_array($element)) {
			echo "dump associative array: ";
			var_dump($element);
			throw new Exception('json cannot be associative array');
		} else {
			throw new Exception('json cannot be '.gettype($json));
		}
	}


	private $_data;
	private $_paths;
}




?>
